package com.anmong.cloud.cloudgateway.filter;
import com.anmong.cloud.cloudgateway.util.IpUtil;
import com.anmong.cloud.cloudgateway.util.ServletUtil;
import com.anmong.cloud.mq.constant.HttpEnum;
import com.anmong.cloud.mq.constant.MqEnum;
import com.anmong.cloud.mq.dto.AccessLogDTO;
import com.anmong.cloud.mq.rocketmq.MqSimpleProducer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;

/**
 * @author ：Shine
 * @description：
 * @date ：2021/6/24 9:40
 */
@Component
@Slf4j
public class AccessLogGlobalFilter implements GlobalFilter, Ordered {
    private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    /*
    *为了使消息有序性(非严格有序)
     */
    private String hashKey = String.valueOf(this.hashCode());

    @Autowired
    private MqSimpleProducer mqSimpleProducer;
    @Autowired
    private ObjectMapper objectMapper;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        AccessLogDTO dto = new AccessLogDTO();

        ServerHttpRequest request = exchange.getRequest();

        //获取请求的ip，url，method，body
        String requestPath = request.getPath().pathWithinApplication().value();
        String clientIp = IpUtil.getIpAddress(request);
        String scheme = request.getURI().getScheme();
        String method = request.getMethod().toString();
        String token = request.getHeaders().getFirst(HttpEnum.Header.认证TOKEN.code);
        String requestId = request.getHeaders().getFirst(HttpEnum.Header.请求ID.code);
        dto.setUrl(requestPath);
        dto.setMethod(method);
        dto.setAuthToken(token);
        dto.setIpAddress(clientIp);
        dto.setRequestTime(LocalDateTime.now());
        dto.setRequestId(requestId);
        String contentTypeString = null;
        MediaType contentType = request.getHeaders().getContentType();
        if (contentType != null){
            contentTypeString = contentType.toString();
        }
        if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType) || MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
            return writeBodyLog(exchange, chain, dto);
        } else {
            return writeBasicLog(exchange, chain,contentTypeString, dto);
        }
    }

    private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain,String contentType, AccessLogDTO accessLog) {
        //文件上传的body不解析
        if (!StringUtils.equals(MediaType.MULTIPART_FORM_DATA_VALUE,contentType)){
            MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
            accessLog.setParameter(ServletUtil.getUrlParamsByMap(queryParams));
        }

        //    获取响应体
        ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
        return chain.filter(exchange.mutate().response(decoratedResponse).build())
                .then(Mono.fromRunnable(() -> {
                    //打印日志
                    writeAccessLog(accessLog);
                }));
    }


    /**
     * 解决request body 只能读取一次问题
     *
     * @param exchange
     * @param chain
     *
     * @return
     */
    private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, AccessLogDTO dto) {
        ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                .flatMap(body -> {
                    dto.setParameter(body);
                    return Mono.just(body);
                });

        // 通过 BodyInsert 插入 body(支持修改body), 避免 request body 只能获取一次
        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);

        return bodyInserter.insert(outputMessage, new BodyInserterContext())
                .then(Mono.defer(() -> {
                    // 重新封装请求
                    ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);

                    // 记录响应日志
                    ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, dto);

                    // 记录普通的
                    return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
                            .then(Mono.fromRunnable(() -> {
                                // 打印日志
                                writeAccessLog(dto);
                            }));
                }));
    }


    /**
     *
     *
     */
    private void writeAccessLog(AccessLogDTO dto) {
        try {
            mqSimpleProducer.sendAsyncOrderlyMsgWithTag(MqEnum.Topic.日志.code,MqEnum.Tag.访问日志.code, objectMapper.writeValueAsString(dto),hashKey);
        } catch (JsonProcessingException e) {
            log.error("发生mq消息出错:{}",e.getMessage());
        }
    }

    /**
     * 请求装饰器，重新计算 headers
     *
     * @param exchange
     * @param headers
     * @param outputMessage
     * @return
     */
    private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers,
                                                       CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0) {
                    httpHeaders.setContentLength(contentLength);
                } else {
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }

            @Override
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }

    /**
     * 记录响应日志
     *
     * @param exchange
     * @return
     */
    private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLogDTO dto) {
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();

        return new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    LocalDateTime now = LocalDateTime.now();
                    dto.setResponseTime(now);
                    Duration duration = Duration.between(dto.getRequestTime(),now);
                    // 计算执行时间
                    long executeTime = duration.toMillis();
                    dto.setExecuteTime(executeTime);
                    dto.setStatusCode(this.getStatusCode().value());
                    // 获取响应类型，如果是 json 就打印
                    String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);

                    if (ObjectUtils.equals(this.getStatusCode(), HttpStatus.OK)
                            && StringUtils.isNotBlank(originalResponseContentType)
                            && originalResponseContentType.contains("application/json")) {

                        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {

                            // 合并多个流集合，解决返回体分段传输
                            DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                            DataBuffer join = dataBufferFactory.join(dataBuffers);
                            byte[] content = new byte[join.readableByteCount()];
                            join.read(content);

                            // 释放掉内存
                            DataBufferUtils.release(join);
                            String responseResult = new String(content, StandardCharsets.UTF_8);
                            dto.setResponseBody(responseResult);

                            return bufferFactory.wrap(content);
                        }));
                    }
                }
                return super.writeWith(body);
            }
        };
    }


    @Override
    public int getOrder() {
        return -1;
    }
}
